﻿using System;
using System.Collections.Generic;
using System.Linq;
using Itenso.TimePeriod;
using LogManager.Base;
using LogManager.Base.Data;
using LogManager.Base.Models.Projections;
using LogManager.Helpers;
using LogManager.LocalStorage.Indexes;
using Raven.Abstractions.Data;
using Raven.Client;
using Raven.Client.Linq;
using Raven.Json.Linq;
using EventsPerLog = LogManager.LocalStorage.Indexes.Events_PerLog;
using Raven.Client.Embedded;
using System.Threading.Tasks;

namespace LogManager.LocalStorage.Data
{
    public class LocalEventService : IEventService
    {
        private readonly IDocumentStore _store;
        private readonly ILogService _logService;

        public LocalEventService(ILogService logService)
        {
            _logService = logService;

            //Init store
            _store = new EmbeddableDocumentStore
                         {
                             DataDirectory = "~/App_Data/LocalStorage",
                         }.Initialize().CreateIndexes();
        }

        #region Import

        public void AsyncImport(DateTime start, DateTime end, IEventExportService exportService)
        {
            var task = Task.Factory.StartNew(() => exportService.Export(start, end))
                .ContinueWith(t => Import(t.Result,exportService.ServerName,exportService.LogName,start,end));
        }

        public void Import(IEnumerable<IEvent> importableEvents, string serverName, string logName, DateTime start, DateTime end)
        {
            int expectedAmount = importableEvents.Count();

            if(expectedAmount > 0)
            {
                using (var session = _store.OpenSession())
                {
                    //Set a max
                    session.Advanced.MaxNumberOfRequestsPerSession = 200;

                    //Create incomplete import
                    var import = new EventImport
                                     {
                                         ImportDate = DateTime.Now,
                                         LogName = logName,
                                         ServerName = serverName,
                                         Start = start,
                                         End = end,
                                         Completed = false,
                                         ExcpectedAmount = expectedAmount
                                     };
                    //Store import
                    session.Store(import);
                    session.SaveChanges();

                    //Import all events in batches and update import record
                    ImportEventBatches(import,importableEvents, session);

                    //Set the new import information
                    import.Completed = true;

                    //Update the import that it is completed and the amount
                    PatchImportComplete(import);
                   

                    session.SaveChanges();
                }
            }
        }

        /// <summary>
        /// Imports importable event in batches and stored and patches amount of imported events
        /// </summary>
        /// <param name="import">Related import</param>
        /// <param name="importableEvents">Events to import</param>
        /// <param name="session">Session to import</param>
        private void ImportEventBatches(EventImport import,IEnumerable<IEvent> importableEvents, IDocumentSession session)
        {
            int counter = 0;

            foreach (var e in importableEvents)
            {
                //Dont let key generate, override if already exists
                e.Id = RavenIdHelper.GenerateId(e.ServerName, e.LogName, e.Id);
                session.Store(e);

                counter++;

                if (counter % 128 == 0)
                {
                    import.Amount = counter;
                    PatchImportAmount(import);
                    session.SaveChanges();
                }
            }

            import.Amount = counter;
        }

        /// <summary>
        /// Update amount of an import
        /// </summary>
        /// <param name="import">Import to patch</param>
        private void PatchImportAmount(EventImport import)
        {
            _store.DatabaseCommands.Patch(
                import.Id,
                new[]
                    {
                        new PatchRequest
                            {
                                Type = PatchCommandType.Set,
                                Name = "Amount",
                                Value = new RavenJValue(import.Amount),
                            }
                    });
        }

        /// <summary>
        /// Update the import completed flag and amount
        /// </summary>
        /// <param name="import"></param>
        private void PatchImportComplete(EventImport import)
        {
            _store.DatabaseCommands.Patch(
                import.Id,
                new[]
                    {
                        new PatchRequest
                            {
                                Type = PatchCommandType.Set,
                                Name = "Completed",
                                Value = new RavenJValue(import.Completed),
                            },
                        new PatchRequest
                            {
                                Type = PatchCommandType.Set,
                                Name = "Amount",
                                Value = new RavenJValue(import.Amount),
                            }
                    });
        }

        private IEnumerable<IEventImport> GetImports(string serverName,string logName)
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<EventImport>()
                    .Where(i => i.ServerName == serverName && i.LogName == logName)
                    .OrderByDescending(i => i.ImportDate);
            }
        }

        /// <summary>
        /// Synchronises a log asynchroniously
        /// </summary>
        /// <param name="log">Log to syncrhonise</param>
        /// <param name="dateRange">Period to synchronise</param>
        /// <param name="tolerance">If tolreance is 5 then only all 5 minutes will be synced</param>
        private void AsyncSyncLog(ILog log, ITimePeriod dateRange, int tolerance = 1)
        {
            var requestedRange = new TimeRange(dateRange.Start, dateRange.End);
            var previousImports = GetImports(log.ServerName, log.LogName).ToList();

            if (previousImports.Count > 0)
            {

                //Add 1 minute tolerance
                var lastImportEnding = previousImports.Max(i => i.End);
                if (requestedRange.End.AddMinutes(tolerance) < lastImportEnding)
                    requestedRange.ShrinkEndTo(lastImportEnding);

                //Create new range with the given imports
                //that intersect with the requested range
                var importedRange = new TimePeriodCollection(previousImports
                    .Select(i => new TimeRange(i.Start, i.End))
                    .Where(t => t.IntersectsWith(requestedRange)));

                var requiredImports = GetRequiredImports(requestedRange, importedRange, previousImports);

                //Do the imports
                AsyncExecuteRequiredImports(log, dateRange, importedRange, requiredImports);
            }
            else
            {
                //Sync the whole requested range if nothing exists
                AsyncImport(dateRange.Start, dateRange.End, log.ExportService);
            }
        }

        private static ITimePeriodCollection GetRequiredImports(ITimePeriod requestedRange, TimePeriodCollection importedRange, IEnumerable<IEventImport> previousImports)
        {
            var gapCalculator = new TimeGapCalculator<TimeRange>();
            var requiredImports = gapCalculator.GetGaps(importedRange);

            //If there are gaps add import before and after
            if (requiredImports.Count > 0)
            {
                if (requiredImports.Start > requestedRange.Start)
                    requiredImports.Add(new TimeRange(requestedRange.Start, requiredImports.Start));

                if (requiredImports.End < requestedRange.End)
                    requiredImports.Add(new TimeRange(requiredImports.End, requestedRange.End));
            }
            else
            {
                //If no gaps get the min start and max end
                var lastEnd = previousImports.Max(i => i.End);
                var firstStart = previousImports.Min(i => i.Start);

                if (firstStart > requestedRange.Start)
                    requiredImports.Add(new TimeRange(requestedRange.Start, firstStart));

                if (lastEnd < requestedRange.End)
                    requiredImports.Add(new TimeRange(lastEnd, requestedRange.End));
            }
            return requiredImports;
        }

        private void AsyncExecuteRequiredImports(ILog log, ITimePeriod dateRange, TimePeriodCollection importedRange, ITimePeriodCollection requiredImports)
        {
            var imports = new List<IEventImport>();

            //Check if there is any real gap
            if (importedRange.Count > 0)
            {
                foreach (var import in requiredImports)
                {
                    AsyncImport(import.Start, import.End, log.ExportService);
                }
            }
            else
            {
                AsyncImport(dateRange.Start, dateRange.End, log.ExportService);
            }
        }

        /// <summary>
        /// Synchronises depending on parameters everything or only events for a server or log
        /// There can should be only one sync at a time
        /// </summary>
        /// <param name="dateRange">Period to synchronize</param>
        /// <param name="serverName">Name of the server</param>
        /// <param name="logName">Name of the log</param>
        private void AsyncSync(ITimePeriod dateRange, string serverName = null, string logName = null)
        {   
            var logs = !string.IsNullOrEmpty(serverName) && !string.IsNullOrEmpty(logName) ? new List<ILog> { _logService.Get(serverName,logName) } : _logService.All();
            foreach (var log in logs)
            {
                //Sync the log
                AsyncSyncLog(log, dateRange);
            }            
        }

        #endregion

        #region Implementation of IDisposable

        public void Dispose()
        {
            _store.Dispose();
        }

        #endregion

        #region Implementation of IEventService

        public IEvent GetEvent(string serverName, string logName, string id)
        {
            //No import needed because it should be in storage when user already has id

            using (var session = _store.OpenSession())
            {
                var @event = session.Load<Event>(RavenIdHelper.GenerateId(serverName, logName, id));
                return @event;                
            }
        }

        public IEnumerable<EventsPerDataProjection> GetEventsPerDate()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<EventsPerDataProjection, Events_PerDate>();
            }
        }

        public IEnumerable<EventsPerLogProjection> GetEventsPerLog()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<EventsPerLogProjection,EventsPerLog>().AsEnumerable();
            }
        }

        public IEnumerable<EventsTypesPerLogProjection> GetEventsType()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<EventsTypesPerLogProjection, Indexes.Events_TypePerLog>().AsEnumerable();
            }
        }

        public IEnumerable<LevelsPerDateProjection> GetLevelsPerDate()
        {
            using (var session = _store.OpenSession())
            {
                var resultlist = new List<LevelsPerDateProjection>();
                var query = session.Query<LevelsPerDateProjection, Indexes.Events_LevelsPerDate>();

                //Dirty workaround for limit
                foreach (var p in query.Take(120))
                {
                    resultlist.Add(p);
                }

                return resultlist;
            }
        }

        public IEnumerable<LevelsPerLogProjection> GetLevelsPerLog()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<LevelsPerLogProjection, Indexes.Events_LevelsPerLog>().AsEnumerable();
            }
        }

        public IEnumerable<string> GetUsers()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<UserIndex,Events_User>().Select(ui => ui.User);
            }
        }

        public IEnumerable<EventsPerPathProjection> GetEventsPath()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<EventsPerPathProjection, Events_PerPath>();
            }
        }

        public IEnumerable<string> GetLevels()
        {
            using (var session = _store.OpenSession())
            {
                return session.Query<LevelIndex,Events_Level>().Select(li => li.Level);
            }
        }

        public IQueryable<IEvent> GetEvents(ITimePeriod dateFilter, string serverName, string logName)
        {
            AsyncSync(dateFilter, serverName, logName);
            
            using (var session = _store.OpenSession())
            {
                //Wait for 3 seconds maximum
                var ravenQuery = session.Query<Event>();
                return ravenQuery.Where(e => e.TimeStamp > dateFilter.Start && e.TimeStamp < dateFilter.End).Filter(serverName,logName);
            }
        }

        public IEnumerable<IEventImport> GetImports()
        {
            using (var session = _store.OpenSession())
            {                
                return session.Query<EventImport>().OrderByDescending(ei => ei.ImportDate);  
            }
        }

        #endregion



        
    }
}
